Tasos image React live logo

Server-Sent Events

tasos@kadena.io

September 29, 2023

whois “Tasos Bitsios”

whois “Kadena”

Server-Sent Events (SSE)

A server-push protocol

Use cases

Replaces polling. Stream any kind of update from the server.

What is this new thing?

🥳 SSE is 19 years old

🔧 13 years of mainstream support


W3C Publication History


Current: HTML Living Standard § 9.2

Playground repo - download me!

Play-along repository:

  • basic SSE server (express)
  • react app
  • presentation

https://github.com/takadenoshi/sse-presentation

Useful for examining behaviors, browser implementation differences.

Repo link in QR ➡

Minimum Viable SSE response

The simplest server-sent event stream specifies just data events.

Example with 2 events:

> GET /stream/hello HTTP/1.1

  < HTTP/1.1 200 OK
  < Content-Type: text/event-stream

  < data: Hello\n\n

  < data: ReactLive are you there?\n\n

Content-Type is text/event-stream

Events separated by two newline characters \n\n

Data is encoded in UTF-8 (mandatory)

Playground: “simple” scenario

Simple EventSource consumer

Server-sent events are consumed with EventSource:

let i=0;

  const source = new EventSource("http://localhost:3001/stream/simple");

  // "message" event emitted for each "data" event received
  source.addEventListener("message", (event) => console.log(++i, event.data), false);
  

The “minimum viable response” from the previous slide would trigger the callback twice, logging:






  1 Hello

  2 ReactLive are you there?
> GET /stream/hello HTTP/1.1

  < HTTP/1.1 200 OK
  < Content-Type: text/event-stream

  < data: Hello\n\n

  < data: ReactLive are you there?\n\n

Named Events

You can “namespace” your events using the event field with any custom name:

< event: goal
  < data: "ARS-LIV 1-1 45"\n\n

  < event: spectator-chat
  < data: "Did you see that ludicrous display just now"\n\n

The goal and spectator-chat events are handled separately on the frontend

Comments

Any lines starting with : (colon) are interpreted as comments

< data: this or that\n\n

  < :TODO emit some events in the near future

These are ignored on the client-side

Reconnection (1)

By default*, EventSource consumers will reconnect if the connection is interrupted.

* with implementation-specific caveats

The default reconnection timeout is up to each browser (empirically: between 3-5 s.)

Custom timeouts

The reconnection timeout can be customized from the server-side by emitting a retry: field in any of the events.

retry: 2500
  data: Hello!\n\n
  

Value is in ms; timeouts are linear.

Playground: “Retry-flaky” scenario

Reconnection (2)

Computer can say no

A server can signal “do not reconnect”:

Playground: “Not SSE” scenario

Reconnection (3) - Last-Event-ID

Events can include an id field with any UTF-8 string as value.

Connection interrupted? Reconnection header Last-Event-ID set to the last id received.

This allows the server to resume gracefully.


If this is the last event received in a stream that disconnects:

< id: data-0
  < retry: 5000
  < data: Data Zero event\n

Then the connection timeout will be 5 seconds, and the Last-Event-ID header will be set to data-0.

> GET /stream/notifications HTTP/1.1
  > Host: localhost:3001
  > Last-Event-ID: data-0

Minor caveat later on.

Playground: “notifications” scenario

Full SSE response

Entire SSE gramar: 4+1 fields

  • Setting reconnection time retry: 2000
  • Event identifiers id: 0
  • Unnamed events data: Hello\n\n
  • Comment: starts with colon :I am a ...
  • Named events event: status
> GET /stream/hello HTTP/1.1

  < HTTP/1.1 200 OK
  < Content-Type: text/event-stream

  < retry: 2000
  < id: 0
  < data: Hello\n\n

  < :I am a comment line

  < id: 1
  < event: status
  < data: {"warn":"Service degraded"}\n\n

More EventSource consumer

You can subscribe to custom events (e.g. status) with .addEventListener:

const source = new EventSource('/stream/hello');

  // [name]: triggers for custom named event, here: "status"
  source.addEventListener(
    "status",
    ({ data }) => console.log("custom event: status", JSON.parse(data)),
    false,
  );

  // as before, un-named data events
  source.addEventListener("message", (event) => { console.log("data event", event.data); }, false);

Subscribe to open and error for connection management:

source.addEventListener("open", (event) => { console.log("Connection opened"); }, false);

  source.addEventListener("error", (event) => { console.log("Connection error"); }, false);

The EventSource Interface

MDN Reference

Error event is a bit useless

Implementation Considerations: HTTP/1.1 connections quota

Browsers implement a per-hostname connection quota (6) for HTTP/1.1

SSE over HTTP/1.1 may hit this easily with multiple tabs open

Per MDN:

Warning: When not used over HTTP/2, SSE suffers from a limitation to the maximum number of open connections, which can be especially painful when opening multiple tabs, as the limit is per browser and is set to a very low number (6). The issue has been marked as “Won’t fix” in Chrome and Firefox. This limit is per browser + domain, which means that you can open 6 SSE connections across all of the tabs to www.example1.com and another 6 SSE connections to www.example2.com (per Stackoverflow).

When using HTTP/2, the maximum number of simultaneous HTTP streams is negotiated between the server and the client (defaults to 100).

Solutions

Implementation Considerations: Reconnecting

Default reconnection behavior implementation is not fully standardized

When a network error is encountered:

Consider handling reconnections yourself:


Test it out in the playground repo:

Implementation Considerations: Proxies (1)

Proxies can kill

Proxies, load balancers and other networking middleware can kill idle connections after a short while.


Two approaches to fix this:

1/ Comment (not client-aware)

You can emit a comment (any line starting with a colon :)

< :bump

Good enough to keep connection alive.

EventSource won’t emit any event.

Implementation Considerations: Proxies (2)

Proxies can kill

Proxies, load balancers and other networking middleware can kill idle connections after a short while.


Two approaches to fix this:

2/ Heartbeat event (client-aware)

Emit a custom “heartbeat” or “ping” event every 15 seconds or so:

< event: heartbeat
  < data: ""

(data field must be present)

The client can listen to this event and use it to detect stale connections:

“expect heartbeats every N seconds, otherwise reconnect”

Preferred approach, especially for important payloads.

Implementation Considerations: Service Workers (Firefox)

Firefox Service Workers 💔 EventSource

Firefox has yet to implement support for EventSource in its Service Worker context.

Future people can track the present validity of this statement here.

✅ But you can use it in a SharedWorker

Implementation Considerations: Last-Event-ID detail

If no event is emitted in the subsequent connection’s lifetime, the Last-Event-ID is reset.


When a reconnected session is initialized with Last-Event-ID: Some-id

And the connection emits no messages for its lifetime,

Then the Last-Event-ID value is reset.

Vs competiting options

1a/ Polling

Keep requesting new data on an interval

  • Slower
  • Usually more resource intensive than SSE

Benefit: Doesn’t “hog” a connection (HTTP/1.1)

1b/ Long Polling

“Hanging GET” - server keeps connection open/hanging until there is something to write.

Client loops the GET request.

2/ SSE

  • Like formalized, reusable long polling
  • HTTP + REST compatible
    • Works with your existing framework
    • Works with your existing auth
  • Reconnecting

3/ Websockets

  • Not HTTP/REST
    • Websocket server usually a separate beast
  • Must bring your own:
    • Routing
    • Auth
    • Error handling
    • TLS Certs (duplicated)
  • Pain to debug
  • Bidirectional/full duplex
    • good if you need it

Kadena use case

Can I use?

Yes (96.11%)

https://caniuse.com/eventsource

Almost Done

References

§ 9.1 MessageEvent Interface - HTML Living Standard

§ 9.2 Server-Sent Events - HTML Living Standard

Presentation

Presentation source & SSE playground - Github

Font

Monospace font: Kode mono by Kadena’s Isa Ozler